It's a todo list.
1<script lang="ts">
2 import { onMount, tick } from "svelte";
3 import { page } from "$app/stores";
4 import { local_lists, pinned_list, generateId, type List } from "$lib/stores.svelte";
5 import { goto, pushState } from "$app/navigation";
6 import toast, { Toaster } from "svelte-french-toast";
7
8 let is_menu_open = $state(false);
9 let list : List | undefined = $state();
10 let task_input = $state("");
11 let user_lists = $derived(local_lists.value) as List[];
12
13 onMount(() => {
14 list = local_lists.value!.find((l) => l.id === $page.params.id);
15 });
16
17 // since list points to something inside local_lists,
18 // it will run when list state changes
19 $effect(() => local_lists.update());
20
21 function addTask() {
22 if (task_input.length === 0) {
23 toast.error("Enter a task to add");
24 return;
25 }
26
27 list?.tasks.push({
28 id: generateId(),
29 description: task_input,
30 is_completed: false
31 });
32
33 task_input = "";
34 }
35
36 function deleteTask(id: string) {
37 if (list) {
38 list.tasks = list.tasks.filter((t) => t.id !== id);
39 }
40 }
41
42 function createList() {
43 const new_list = {
44 id: generateId(),
45 title: "",
46 tasks: []
47 };
48
49 local_lists.value!.push(new_list);
50 list = local_lists.value!.find((l) => l.id === new_list.id);
51 goto(`/${list!.id}`);
52 }
53
54 function switchToList(id: string) {
55 list = local_lists.value!.find((l) => l.id === id);
56 goto(`/${list!.id}`);
57 }
58
59 function pinList(id: string) {
60 pinned_list.value = id;
61 }
62
63 function deleteList() {
64 if (pinned_list.value === $page.params.id) {
65 toast.error("Cannot delete pinned list");
66 return;
67 }
68
69 local_lists.value = local_lists.value!.filter((l) => l.id !== $page.params.id);
70 list = local_lists.value.find((l) => l.id === pinned_list.value);
71 goto(`/${list!.id}`);
72 }
73</script>
74
75<main class="flex flex-col w-full px-2 pt-8 pb-12 lg:p-4 lg:pb-24 gap-8 text-xl lg:text-3xl">
76 {#if list}
77 <section class="relative flex gap-4 w-full">
78 <div class="flex gap-4 border-black border w-fit h-fit p-2 bg-white rounded-xl">
79 <button onclick={() => is_menu_open = !is_menu_open}>
80 <img
81 src="/list-box-line.svg"
82 alt="Lists button"
83 class="w-12 h-12 hover:bg-slate-500/10 rounded-full"
84 />
85 </button>
86 <button onclick={() => pinList(list!.id)}>
87 <img
88 src={pinned_list.value === list.id ? "/pin.svg" : "/pin-line.svg"}
89 alt="Pin list button"
90 class="w-12 h-12 hover:bg-slate-500/10 rounded-full"
91 />
92 </button>
93 <button onclick={deleteList}>
94 <img
95 src="/trash-line.svg"
96 alt="Delete list button"
97 class="w-12 h-12 hover:bg-slate-500/10 rounded-full"
98 />
99 </button>
100 </div>
101
102 {#if is_menu_open}
103 <menu class="absolute flex flex-col gap-2 w-fit h-fit top-20 p-2 bg-white border border-black rounded-lg !text-black !text-lg">
104 {#each user_lists as user_list : List (user_list.id)}
105 <button
106 onclick={() => {
107 switchToList(user_list.id)
108 is_menu_open = false;
109 }}
110 class="flex gap-2 justify-between text-start w-full h-full rounded-xl pl-2 pr-5 py-2 hover:bg-slate-500/10 transition-all duration-150 items-center"
111 >
112 {user_list.title.length > 0 ? user_list.title : "Untitled"}
113 {#if user_list.id === list.id}
114 <img src="/check-line.svg" alt="Item 1" class="w-8 h-8" />
115 {/if}
116 </button>
117 {/each}
118 <button
119 onclick={() => {
120 createList();
121 is_menu_open = false;
122 }}
123 class="flex gap-2 justify-between text-start w-full h-full rounded-xl pl-2 pr-5 py-2 hover:bg-slate-500/10 transition-all duration-150 items-center"
124 >
125 Create new list
126 </button>
127 </menu>
128 {/if}
129 </section>
130 <input
131 type="text"
132 bind:value={list.title}
133 placeholder="Untitled"
134 class="text-5xl font-bold bg-transparent"
135 />
136 <ul class="flex flex-col gap-4">
137 {#each list.tasks as task (task.id)}
138 <li class="group flex justify-between items-center gap-4">
139 <div class="flex w-full gap-4 items-center pr-4 py-2">
140 <input
141 type="checkbox"
142 bind:checked={task.is_completed}
143 class="w-6 h-6 bg-transparent"
144 />
145 <input
146 type="text"
147 bind:value={task.description}
148 class="w-full hover:underline text-ellipsis overflow-hidden bg-transparent"
149 />
150 </div>
151
152 <div class="flex lg:hidden group-hover:flex gap-4 w-fit">
153 <button
154 onclick={() => deleteTask(task.id)}
155 class="px-4 py-2 bg-red-500 rounded-xl text-white"
156 >
157 -
158 </button>
159 </div>
160 </li>
161 {/each}
162 <li class="flex gap-4 w-full">
163 <button onclick={addTask} class="px-5 rounded-full bg-white text-black">
164 +
165 </button>
166 <input type="text" bind:value={task_input} class="bg-transparent pr-4 py-2 border-b w-full"/>
167 </li>
168 </ul>
169
170 {:else}
171 <p>Loading...</p>
172 {/if}
173</main>